/**
 * \file
 */
#ifndef kfs_impl_hpp_included
#define kfs_impl_hpp_included
#include "kfs.hpp"
#include "kfs_math.hpp"
#include <boost/scoped_array.hpp>
#include <boost/cstdint.hpp>
#include <boost/function.hpp>
#include <map>
#include <string>
#include <vector>

extern block_index_t const blockid_nil;

class MasterBlock
{
public:

    static uint32_t Magic;

    //uint32_t magic;
    uint32_t numBlocks;
    uint16_t blockNumBytes;
    //uint8_t blockNumBits;
};

//uint32_t blockNumBytes(MasterBlock const& mb)
//{
//    return 1 << mb.blockNumBits;
//}

class AllocBlockRef;
class CacheInode;
class CacheRef;
class DirIter;
class FileDescriptor;

class kfs::Impl
{
public:

    typedef boost::function<bool(file_size_t, std::string const&)> DirVisitor;

    typedef std::map<std::string, boost::shared_ptr<CacheInode> > InodeCache;

    Impl();

    /**
     * Reference a newly-created inode using the in-memory copy provided by the
     * caller.
     */
    CacheRef*
    cache_add_from_memory(InodeCache::value_type const& elt);

    /**
     * Retrieve an in memory copy of the inode that belongs to the path.
     *
     * Semantics: find or create.
     *
     * The path must be absolute.
     *
     * @return A reference into the inode cache. The caller is responsible to
     * delete the CacheRef.
     */
    CacheRef*
    cache_get(std::string const& path);

    /**
     * Reference an inode on disk.
     *
     * Rather than traversing the path, trust the caller to supply the inode
     * number.
     */
    CacheRef*
    cache_get_from_disk(block_index_t const& inodeNumber,
        std::string const& path);

    void cache_destroy();

    void cache_flush(CacheInode* inodeRef); //InodeCache::value_type& elt);

    void cache_remove(InodeCache::iterator elt);

    /**
     * Add an entry to the directory.  Fail if an entry exists with that name.
     *
     * @throw Error(EEXIST) if there is already an entry with that name
     */
    void
    dir_add(FileDescriptor* dir, std::string const& name,
        block_index_t childInodeNum);

    /**
     * Look up an entry in the directory represented by the desired inode.  If
     * there is a matching entry then the details are copied into dirIter.
     * Otherwise dir_find throws an Error exception.
     *
     @ throw Error(ENOENT) if there is no entry of that name
     * @return A reference to the matching directory entry
     */
    void
    dir_find(FileDescriptor const* directory, std::string const& name,
        DirIter* dirIter);

    uint32_t dir_numEntries(FileDescriptor const* directory);

    void
    dir_remove(FileDescriptor* dir, file_size_t offset);

    bool
    dir_safeToDelete(FileDescriptor const* dir);

    /**
     * Visit entries in the directory indicated by the inode reference.  The
     * caller supplies a call-back which is invoked for each entry.  The
     * traversal stops when the call-back returns false or the final entry
     * is visited.
     *
     * @param offset Offset of first entry to be visited.  Zero: start from the
     *        beginning of the directory.
     */
    void dir_visitEntries(
        FileDescriptor const* dir, file_size_t offset, DirVisitor
    );

    /**
     * @return block_index_t diskBlockNumber
     */
    AllocBlockRef disk_allocate();

    /**
     * Allocate a set of n blocks.  If there are not at least n available
     * blocks then no blocks are allocated.
     *
     * @param output If successful, output contains n block references.
     * @return void Return if successful, otherwise throw an exception.
     * @throw Error(ENOSPC) if n blocks could not be allocated
     */
    void disk_allocate(block_index_t n, std::vector<AllocBlockRef>* output);

    /**
     * Allocate a contiguous sequence of n blocks.  If no sequence of length
     * at least n exists then no blocks are allocated.
     * @return void Return if successful, otherwise throw an exception.
     */
    block_index_t disk_allocate_contiguous(block_index_t n);

    void disk_allocate_help(block_index_t* bitmap, block_index_t n,
        std::vector<block_index_t>* list);

    /**
     * Free a block.
     * @param diskBlockNumber
     */
    void disk_free(block_index_t diskBlockNumber);

    /**
     * Free a set of blocks.
     * @param diskBlockNumber
     */
    template<typename iterator>
    void disk_free(iterator begin, iterator end);

    FileDescriptor* fd_deref(uint64_t fileDescriptor);

    void fd_release(uint64_t fileDescriptor);

    /**
     * Add numBytes to the end of the file.  If successful the file's size
     * will have increased by exactly numBytes.
     *
     * TODO: Improve atomicity guarantee in case of free space exhaustion.
     */
    void file_allocator_grow(CacheRef& inode, file_size_t numBytes,
        std::vector<block_index_t>* diskBlockNums);

    void file_allocator_shrink(CacheRef& inodeRef);

    /**
     * Compute the sequence of table indices that, for any file, would point to
     * the disk block number that contains the file's desired data.
     *
     * @param fBlockNum A block number in the file. Block numbers are relative
     *        to the file, and they start at zero.
     *
     * @param indices A vector in which to place the result.
     */
    void file_getIndices(uint32_t fBlockNum, std::vector<uint32_t>& indices);

    /**
     * @param inode The inode of a file.
     * @param fBlockNum The address of a block in the file described by inode.
     * @return The address of the disk block that corresponds with the
     *         desired file block
     */
    uint32_t file_resolveDiskBlock(CacheRef const& inodeRef, uint32_t fBlockNum);

    /**
     * @param inode The root of the index for the desired file.
     * @return Number of data blocks in this file.
     */
    inline block_index_t
    fileNumBlocks(Inode const& inode)
    {
        return static_cast<block_index_t>(
            div_ceil<file_size_t>(inode.sizeNumBytes, mb.blockNumBytes));
    }

    /**
     * Create a new inode and link it to the given directory.
     *
     * If the inode is to be a directory, use the O_RDWR flag. Otherwise a
     * permission error will arise when initializng the new directory's
     * structure.
     */
    FileDescriptor* inode_create(FileDescriptor* parent,
        kfs* fs, std::string const& path,
        std::string const& name, int flags, mode_t mode, uid_t userID,
        gid_t groupID );

    /**
     * Accept an absolute path.  Traverse the directory structure to retrieve
     * the inode for that path.
     *
     * TODO: Use the inode cache
     *
     * TODO: accept the empty string to mean the root directory.
     * TODO: accept / to mean the root directory.
     *
     * @return A copy of the inode. The caller is responsible to deallocate it.
     */
    InodeRef*
    traverse(std::string const& path, kfs* fs);

    uint8_t*
    createBlockBuffer();

    /**
     * Serializers
     */
    //@{
    uint8_t const*
    deserialize(MasterBlock& mb, uint8_t const* sourceBuffer);

    uint8_t*
    serialize(MasterBlock const& mb, uint8_t* destBuffer);

    uint8_t const*
    deserialize(Inode& inode, uint8_t const* sourceBuffer);

    uint8_t*
    serialize(Inode const& inode, uint8_t* destBuffer);
    //@}

    std::fstream f;
    MasterBlock mb;
    block_index_t rootBlockNum;
    boost::scoped_array<uint32_t> fat;
    InodeCache inodeCache;

    // HACK
    uint32_t bitmapDiskBlockNum;
};

class DirIter
{
public:
    /**
     * The location of the entry in the directory's data structure.
     */
    file_size_t offset;
    /**
     * The number of the disk block where the inode is stored.
     */
    block_index_t blockNum;
};

//
// Path parsing
//

/**
 * Split a path string into two components.
 *
 * Examples:
 * path                parent path         child name
 * /                   /
 * /foo                /                   foo
 * /foo/bar            /foo/               bar
 * /foo/bar/           /foo/               bar
 * /foo/bar/pole/      /foo/bar/           pole
 *
 * @param path An absolute path
 * @param parentPath The parent path is placed here if successful
 * @param childName The child name is placed here if successful
 * @throw Invalid argument if `path' is not absolute
 */
void
kfs_path_split(
    std::string const& path, std::string& parentPath, std::string& childName
);

//
// Block access
//

/**
 * TODO Use me
 *
 #include <fstream>
class FileAsBlockDevice
{
public:
    FileAsBlockDevice(std::string const& fileName);
    void read(uint8_t* blockData, uint64_t blockNumber);
    void write(uint8_t const* blockData, uint64_t blockNumber);

    uint64_t begin()
    {
        return 0;
    }

    uint64_t end();

    std::fstream file;
};
*/

void kfs_disk_read(kfs::Impl& impl, uint64_t blockID, uint8_t* blockData);
void kfs_disk_write(kfs::Impl& impl, uint64_t blockID, uint8_t const* blockData);

// Fixed = { uint8_t, uint64_t }

template<typename Fixed>
uint8_t const* deserializeFixed(Fixed& dest, uint8_t const* sourceBuffer);

template<typename Fixed>
uint8_t* serializeFixed(Fixed const& source, uint8_t* destBuffer);

template<typename Fixed>
uint8_t const* deserializeFixed(std::vector<Fixed>& dest,
    std::size_t numElements,
    uint8_t const* sourceBuffer);

template<typename Fixed>
uint8_t* serializeFixed(std::vector<Fixed> const& source,
    std::size_t numElements,
    uint8_t* destBuffer);

#endif // kfs_impl_hpp_included
